import { AppError, getInstructionVersion, uploadInstruction } from "@autoscholar/core"; import { INSTRUCTION_MAX_BYTES, UploadInstructionRequest } from "@autoscholar/schemas"; import { handler, json, parseBody } from "@/lib/server/api"; import { db } from "@/lib/server/db"; import { requireAdmin, uploadLimiter } from "@/lib/server/session"; export const runtime = "nodejs"; /** Upload (multipart `file`) or paste (JSON {content}) a research CLAUDE.md. Stores exact bytes + SHA-256 as a new immutable version. */ export const POST = handler(async (req, { params }) => { const sess = await requireAdmin(req, { mutation: true }); if (!uploadLimiter.check(sess.userId).allowed) throw new AppError("rate_limited", "too many uploads", 429); const { id } = (await params) as { id: string }; const ct = req.headers.get("content-type") ?? ""; let bytes: Buffer; let filename = "CLAUDE.md"; let note: string | undefined; if (ct.startsWith("multipart/form-data")) { const len = Number(req.headers.get("content-length") ?? 0); if (len > INSTRUCTION_MAX_BYTES * 2) throw new AppError("too_large", "upload too large", 413); const form = await req.formData(); const file = form.get("file"); if (!(file instanceof File)) throw new AppError("invalid", "multipart field `file` required", 400); if (file.size > INSTRUCTION_MAX_BYTES) throw new AppError("too_large", `file exceeds ${INSTRUCTION_MAX_BYTES} bytes`, 413); const allowed = ["text/markdown", "text/plain", "text/x-markdown", "application/octet-stream", ""]; if (!allowed.includes(file.type)) throw new AppError("invalid", `unsupported content type ${file.type}`, 400); bytes = Buffer.from(await file.arrayBuffer()); filename = file.name || "CLAUDE.md"; const n = form.get("note"); if (typeof n === "string" && n) note = n.slice(0, 500); } else { const body = await parseBody(req, UploadInstructionRequest, INSTRUCTION_MAX_BYTES * 2); bytes = Buffer.from(body.content, "utf8"); filename = body.filename; note = body.note; } const result = await uploadInstruction(db(), id, bytes, filename, note, { id: sess.userId, ip: sess.ip }); const { text: _t, ...validation } = result.validation; return json( { id: result.id, version: result.version, deduplicated: result.deduplicated, validation }, { status: result.deduplicated ? 200 : 201 }, ); }); export const GET = handler(async (req) => { await requireAdmin(req); const url = new URL(req.url); const versionId = url.searchParams.get("versionId"); if (!versionId) throw new AppError("invalid", "versionId required", 400); const v = await getInstructionVersion(db(), versionId); return json({ id: v.id, version: v.version, filename: v.filename, sha256: v.sha256, byteSize: v.byteSize, content: v.contentText, frontmatter: v.frontmatter, validation: v.validation, createdAt: v.createdAt, }); });